#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import json
import os
import stat
import time
import subprocess
import re

from adds.service.backup.adds_sqlite_service import ADSqliteService
from common.number_const import NumberConst
from common.common_models import SubJobDetails, LogDetail
from common.const import RepositoryDataTypeEnum, SubJobStatusEnum, DBLogLevel, ParamConstant, RestoreType
from common.schemas.thrift_base_data_type import SubJob, SubJobType
from adds.comm.common import execute_cmd

from adds.comm.log import log
from adds.comm.const import ADDSLabel, LogLevel, ADDSCode, TaskStatus
from adds.comm.utils import report_job_details_by_rpc, get_powershell_path, remove_file, \
    parse_object_id, parse_object_info, get_obj_param_info, check_obj_param_info, is_normal_mode, \
    bcdedit_delete_safeboot, checkout_recycle_bin_feature, get_obj_path, check_need_to_move, \
    check_need_change_memberof, check_need_to_rename, check_need_change_member, format_value, \
    get_ad_object_info, get_obj_exist_with_distinguishedname, get_previous_task_info, check_task_in_current_time, \
    get_ad_domain_distinguishedname, get_decoding_mode
from adds.schemas.adds_schemas import ResultInfo
from adds.service.adds_service_base import ADDSServiceBase
from adds.service.restore.parse_restore_param import RestoreParam


class ADDSRestoreService(ADDSServiceBase):
    def __init__(self, req_id: str, job_id: str, sub_id: str):
        super().__init__(req_id, job_id, sub_id)
        self.param = RestoreParam(self.req_id)
        self.data_path = self.param.get_data_path(self.param.get_copies()[0]["repositories"],
                                                  RepositoryDataTypeEnum.DATA_REPOSITORY.value)
        self.cache_path = self.param.get_data_path(self.param.get_copies()[0]["repositories"],
                                                   RepositoryDataTypeEnum.CACHE_REPOSITORY.value)
        self.error_code = ADDSCode.FAILED.value
        self.log_label = ""
        self.log_label_param = []
        self.ps_loc = get_powershell_path()
        self.encoding_mode = get_decoding_mode()
        self.restore_type = self.param.get_restore_type()
        self.json_path = self.data_path + "/" + "object.json"
        self.object_json = list()
        self.id_info = dict()
        self.task_err_code = ""
        self.task_err_description = ""
        self.report_time = 0
        self.scan_number = 0
        self.report_flag = True
        self.meta_repository_path = self.param.get_repository_path(RepositoryDataTypeEnum.META_REPOSITORY.value)
        self.search_pattern = r"\((\d+)\)"
        self.recycle_bin_enable = False
        # '0'表示目录树选择， '1'表示用户手动输入
        self.object_from = '0'

    def allow_restore_in_local_node(self):
        """
        功能描述：检查当前节点是否可以执行恢复任务
        """
        log.info(f"allow_restore_in_local_node success, job id:{self.job_id}.")
        response = ResultInfo()
        self.report_result(response.dict(by_alias=True))
        return True

    def restore_prerequisite(self):
        """
        功能描述：恢复前置任务
        """
        log.info(f"restore_prerequisite success, job id:{self.job_id}.")
        response = ResultInfo()
        self.report_result(response.dict(by_alias=True))
        return True

    def restore_prerequisite_progress(self):
        """
        功能描述：备份前置任务进度上报
        """
        log.info(f"backup_prerequisite_progress success, job id:{self.job_id}.")
        progress = SubJobDetails(taskId=self.job_id, subTaskId=self.sub_id, progress=100,
                                 taskStatus=SubJobStatusEnum.COMPLETED.value)
        self.report_result(progress.dict(by_alias=True))
        return True

    def restore_gen_sub_job(self):
        """
        功能描述：分解子任务
        """
        log.info(f"restore_gen_sub_job start, job id:{self.job_id}.")
        response = [
            SubJob(jobId=self.job_id, jobType=SubJobType.BUSINESS_SUB_JOB.value, jobName=str(self.job_id),
                   ignoreFailed=False).dict(by_alias=True)
        ]
        self.report_result(response)
        return True

    def restore(self):
        """
        功能描述：执行恢复任务
        """
        log.info(f"Enter Active Directory restore, job_id: {self.job_id}, sub_id: {self.sub_id}.")
        self.report_restore_start()
        self.recycle_bin_enable = checkout_recycle_bin_feature()
        if self.restore_type == RestoreType.FINE_GRAINED_RESTORE.value:
            if not is_normal_mode():
                self.log_label = ADDSLabel.ADDS_RESTORE_COMPUTER_NOT_IN_NORMAL_MODE
                self.report_sub_job_failed(self.error_code)
                return False
            if os.path.exists(self.cache_path + f"/restore_flag_{self.job_id}"):
                log.info("restore task already running")
                result = self.check_restore_task_status_fine_grained()
                return result
            if not self.report_exist_obj_list():
                return False
            restore_flag = self.restore_object()
            if restore_flag:
                self.update_ad_obj_restored(TaskStatus.COMPLETED.value)
                self.report_sub_job_success()
            else:
                self.update_ad_obj_restored(TaskStatus.FAILED.value)
                self.report_sub_job_failed(self.error_code)
        else:
            if is_normal_mode():
                log.error("computer is not in dsrm")
                self.log_label = ADDSLabel.ADDS_COMPUTER_NOT_IN_DSRM
                self.report_sub_job_failed(self.error_code)
                return False
            if self.check_restore_task_running():
                result = self.check_restore_task_status()
                return result
            restore_flag = self.restore_inner()
            if restore_flag:
                # 整体恢复成功情况下会在restore_inner中重启，因此基本不会走到该分支
                self.report_sub_job_success()
            else:
                self.report_sub_job_failed(self.error_code)
                # 整体恢复上报失败后需要再删除后置写的完成文件
                self.wait_post_complete()
        log.info("Exit restore")
        return restore_flag

    def restore_inner(self):
        restore_start = int(time.time())
        # 仅支持全量备份，因此只需要使用第一个副本数据恢复即可
        if not self.validate_backup_path():
            return False
        target_path = self.param.get_backup_path()
        version = self.get_version(target_path)
        if not version:
            log.error("Get target version failed")
            return False
        command = f"wbadmin start systemstaterecovery -version:{version} -backupTarget:{target_path} -authsysvol -quiet"
        log.info(f"restore_command: {command}")
        process = subprocess.Popen([self.ps_loc, command], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
        self.report_time = int(time.time())
        while True:
            output = process.stdout.readline()
            if output == b'' and process.poll() is not None:
                log.info("Run restore command success")
                break
            if not output.strip():
                continue
            output_str = output.strip().decode(encoding=self.encoding_mode)
            log.info(f"output_str: {output_str}")
            if self.check_win_langage(output_str):
                self.handle_completion(output_str, process)
            else:
                log.warn(f"the language of this Win-Sever is not supported")
            if re.search(r'(\d+).*?%', output_str):
                match_info = re.search(r'(\d+).*?%', output_str)
                progress_info = int(match_info.group(1))
                if not self.report_flag:
                    self.report_scan_progress(self.scan_number)
                    self.report_scan_completed()
                    self.report_flag = True
                current_time = int(time.time())
                if current_time - self.report_time >= NumberConst.SIXTY:
                    self.report_progress(progress_info)
                    self.report_time = current_time
            else:
                self.report_scan_with_output(output_str)
        process.communicate(timeout=NumberConst.THIRTY)
        cmd_code = process.returncode
        if cmd_code != 0:
            log.error(f"Restore failed, cmd_code: {cmd_code}")
            self.format_task_info(restore_start)
            return False
        return True

    def auth_restore(self):
        domain = get_ad_domain_distinguishedname()
        if domain == "":
            log.info("domain is empty")
            return True
        auth_times = self.get_auth_times()
        aucmd = f'ntdsutil "popups off" "Activate Instance ntds" "authoritative restore"'
        for _ in range(auth_times):
            aucmd += f' "restore subtree {domain}"'
        aucmd += " q q"
        log.info(f"anth_command: {aucmd}")
        code, out, err = execute_cmd(cmd=[self.ps_loc, aucmd], timeout=180, cmd_array_flag=True)
        if code == "0":
            log.info("auth restore success")
            return True
        log.error("auth restore failed")
        log.error(f"err:{err}, error_out: {out}")
        return False

    def report_scan_with_output(self, output_str):
        """
        根据不同回显进行上报扫描进度
        """
        if re.search(self.search_pattern, output_str):
            match_info = re.search(self.search_pattern, output_str)
            self.scan_number = int(match_info.group(1))
            current_time = int(time.time())
            if current_time - self.report_time >= NumberConst.SIXTY or self.report_flag:
                self.report_scan_progress(self.scan_number)
                self.report_time = current_time
                self.report_flag = False

    def format_task_info(self, restore_start):
        restore_end = int(time.time())
        task_info = get_previous_task_info()
        task_err_code = task_info.get("DetailedHResult", "0")
        task_err_description = task_info.get("ErrorDescription", "")
        task_start_time = task_info.get("StartTime", "")
        if check_task_in_current_time(restore_start, restore_end, task_start_time):
            self.task_err_code = task_err_code
            self.task_err_description = task_err_description

    def check_samename_object(self):
        """
        恢复AD域对象是否存在同名对象
        """
        exist_objs = []
        try:
            start_time = int(time.time())
            object_infos, self.object_from = self.param.get_object_info()
            obj_set = set()
            for obj_info in object_infos:
                obj_set.add(obj_info)
            obj_list = sorted(list(obj_set))
            sqlite_path = os.path.join(self.meta_repository_path, "sqlite")
            self.object_json = ADSqliteService.get_ad_object_json_info(sqlite_path, obj_list, self.object_from)
            for obj in obj_list:
                current_time = int(time.time())
                if current_time - start_time > NumberConst.SIXTY:  # 检查上次上报时间是否超过一分钟，防止失活
                    self.report_running()
                    start_time = current_time
                obj_id, distinguishedname = parse_object_id(self.object_json, obj, self.object_from)
                # 通过GUID检查对象是否被删除
                obj_info = self.check_obj_is_not_deleted(obj_id)
                # 通过DistinguishedName检查对象是否被删除
                obj_info_by_dn = self.check_obj_is_not_deleted(distinguishedname)
                if not obj_info and obj_info_by_dn:
                    exist_objs.append(distinguishedname)
        except Exception as err:
            log.error(err, exc_info=True)
            return False, []
        return True, exist_objs

    def report_exist_obj_list(self):
        """
        上报已存在同名对象的对象列表
        """
        err_flag, obj_list = self.check_samename_object()
        if not err_flag:
            return False
        if err_flag and obj_list:
            for index, value in enumerate(obj_list):
                obj_list[index] = f'{{{value}}}'
            self.log_label_param = [str(obj_list)]
            self.log_label = ADDSLabel.RESTORE_OBJECT_EXISTED
            self.report_sub_job_failed(self.error_code)
            return False
        return True

    def restore_object(self):
        """
        恢复AD域对象：
        1、对象被删除:(1)、从回收站恢复 (2)、新建对象
        2、对象信息被修改：还原修改的属性
        """
        try:
            start_time = int(time.time())
            self.update_ad_obj_restored(TaskStatus.RUNNING.value)
            object_infos, self.object_from = self.param.get_object_info()
            obj_set = set()
            for obj_info in object_infos:
                obj_set.add(obj_info)
            obj_list = sorted(list(obj_set))
            sqlite_path = os.path.join(self.meta_repository_path, "sqlite")
            self.object_json = ADSqliteService.get_ad_object_json_info(sqlite_path, obj_list, self.object_from)
            for obj in obj_list:
                current_time = int(time.time())
                if current_time - start_time > 60:
                    self.report_running()
                    start_time = current_time
                log.info(f"restore obj: {obj}")
                if not self.restore_single_object(obj):
                    return False
            self.report_running()
            self.set_object_member_and_memberof(obj_list)
        except Exception as err:
            log.error(err, exc_info=True)
            return False
        return True

    def restore_single_object(self, obj):
        obj_id, distinguishedname = parse_object_id(self.object_json, obj, self.object_from)
        # 通过GUID检查对象是否被删除
        obj_info = self.check_obj_is_not_deleted(obj_id)

        # 检查是否开启回收站功能
        if not self.recycle_bin_enable:
            log.warn(f"recycle bin feature not open")
            if obj_info:
                result = self.set_obj_by_data(obj, obj_id, obj_info)
            else:
                result = self.create_obj_by_data(obj_id)
            return result
        if obj_info:
            log.info(f"obj:{obj},id:{obj_id} is not deleted")
            result = self.set_obj_by_data(obj, obj_id, obj_info)
        else:
            if self.check_obj_in_recycle(obj_id):
                result = self.restore_obj_from_recycle(obj_id)
            else:
                result = self.create_obj_by_data(obj_id)
        return result

    def check_obj_is_not_deleted(self, obj_id):
        json_path = os.path.join(self.cache_path, obj_id + ".json")
        check_cmd = f"Get-ADObject -Identity \"{obj_id}\" -Properties * | ConvertTo-Json -Compress | " \
                    f"Out-File -FilePath \"{json_path}\" -Encoding utf8"
        log.info(f"check_cmd:{check_cmd}")
        log.debug(f"check_cmd:{check_cmd}")
        code, out, err = execute_cmd(cmd=[self.ps_loc, check_cmd], encoding="utf-8", timeout=120, cmd_array_flag=True)
        if code == "0":
            with open(json_path, 'r', encoding='utf-8-sig') as f_read:
                obj_info = json.load(f_read)
            remove_file(json_path)
            return obj_info
        else:
            log.info(f"ADObject:{obj_id} is deleted.")
        return dict()

    def check_obj_in_recycle(self, obj):
        search_cmd = f"Get-ADObject -Filter 'isDeleted -eq $true -and ObjectGUID -eq \"{obj}\"' -IncludeDeletedObjects"
        code, out, err = execute_cmd(cmd=[self.ps_loc, search_cmd], timeout=120, cmd_array_flag=True)
        # 若没有输出，表示回收站未找到该对象
        if code == "0" and out:
            return True
        log.debug(f"err_code:{code}; error_out:{out}; err:{err}")
        return False

    def restore_obj_from_recycle(self, obj):
        log.info(f"obj_id:{obj} in recycle bin, restore obj from recycle")
        restore_cmd = f"Restore-ADObject -Identity \"{obj}\""
        code, out, err = execute_cmd(cmd=[self.ps_loc, restore_cmd], timeout=60, cmd_array_flag=True)
        if code == "0":
            return True
        log.error(f"err_code:{code}; error_out:{out}; err:{err}")
        return False

    def create_obj_by_data(self, obj_id):
        log.info(f"obj:{obj_id} not in recycle bin, create obj by restore info")
        param_dict = parse_object_info(self.object_json, obj_id)
        if not param_dict:
            log.error("get ADObject info from json failed !")
            return False
        obj_name = param_dict["Name"] if "Name" in param_dict.keys() else ""
        obj_type = param_dict["ObjectClass"] if "ObjectClass" in param_dict.keys() else ""
        param_info = ""
        obj_path = get_obj_path(self.object_json, obj_id)
        obj_distinguishedname = param_dict["DistinguishedName"] if "DistinguishedName" in param_dict.keys() else ""
        obj_id = param_dict["ObjectGUID"] if "ObjectGUID" in param_dict.keys() else ""
        self.id_info[obj_distinguishedname] = obj_id
        for key, value in param_dict.items():
            if key == "Name" or key == "ObjectClass":
                continue
            # 部分参数中有"-"会被切割
            if "-" in key:
                key = "'" + key + "'"
            param_info += f"{key}={value};"
        if len(param_info) > 0:
            param_info = param_info[:-1]
        create_cmd = f"New-ADObject -Name {obj_name} -Type {obj_type} -Path \"{obj_path}\" " \
                     f"-OtherAttributes @{{{param_info}}} -ProtectedFromAccidentalDeletion $false"
        log.info(f"create_ad_object_cmd: {create_cmd}")
        code, out, err = execute_cmd(cmd=[self.ps_loc, create_cmd], timeout=60, cmd_array_flag=True)
        if code == "0":
            return True
        log.error(f"err_code:{code}; error_out:{out}; err:{err}")
        return False

    def set_obj_by_data(self, obj, obj_id, obj_info):
        set_cmd = f"Set-ADObject -Identity \"{obj_id}\""
        old_info = get_obj_param_info(self.object_json, obj_id)
        param_info = check_obj_param_info(old_info, obj_info)
        if not self.check_param_change(param_info):
            rename_result = self.rename_ad_object(obj_id, obj_info)
            res, path = check_need_to_move(old_info, obj_info)
            if not res:
                # no need to move
                return True
            move_result = self.move_ad_object(obj_id, path)
            if move_result and rename_result:
                return True
            else:
                return False
        add_list, delete_list, change_list, clear_list = param_info[0], param_info[1], param_info[2], param_info[3]
        log.info(f"change_list:{change_list}, delete_list:{delete_list}, add_list:{add_list}")
        if len(change_list) > 0:
            change_info = ";".join(change_list)
            set_cmd += f" -Replace @{{{change_info}}}"
        if len(delete_list) > 0:
            delete_info = ";".join(delete_list)
            set_cmd += f" -Add @{{{delete_info}}}"
        if len(add_list) > 0:
            add_info = ";".join(add_list)
            set_cmd += f" -Remove @{{{add_info}}}"
        if len(clear_list) > 0:
            clear_info = ",".join(clear_list)
            set_cmd += f" -Clear {clear_info}"
        log.info(f"set_obj_cmd: {set_cmd}")
        code, out, err = execute_cmd(cmd=[self.ps_loc, set_cmd], timeout=120, cmd_array_flag=True)
        rename_result = self.rename_ad_object(obj_id, obj_info)
        res, path = check_need_to_move(old_info, obj_info)
        if not res:
            # no need to move
            return True
        move_result = self.move_ad_object(obj_id, path)
        if code == "0" and move_result and rename_result:
            return True
        log.error(f"err_code:{code}; error_out:{out}; err:{err}")
        return False

    def move_ad_object(self, obj_id, path):
        move_cmd = f"Move-ADObject -Identity \"{obj_id}\" -TargetPath \"{path}\""
        log.info(f"move_cmd:{move_cmd}")
        code, out, err = execute_cmd(cmd=[self.ps_loc, move_cmd], timeout=120, cmd_array_flag=True)
        if code == "0":
            return True
        log.error(f"err_code:{code}; error_out:{out}; err:{err}")
        return False

    def change_memberof(self, obj_id, distinguishedname, new_info=None):
        if new_info is None:
            new_info = dict()
        old_info, _ = get_ad_object_info(self.object_json, value=obj_id)
        res, add_list, delete_list = check_need_change_memberof(old_info, new_info)
        if not res:
            return True
        param_info = f"member=\"{distinguishedname}\""
        add_id_list = get_obj_exist_with_distinguishedname(add_list, self.object_json, self.id_info)
        for item in add_id_list:
            if not self.check_obj_is_not_deleted(item):
                continue
            add_cmd = f"Set-ADObject -Identity \"{item}\" -Add @{{{param_info}}}"
            code, out, err = execute_cmd(cmd=[self.ps_loc, add_cmd], timeout=120, cmd_array_flag=True)
            if code != "0":
                log.error(f"err_code:{code}; error_out:{out}; err:{err}")
                return False
        del_id_list = get_obj_exist_with_distinguishedname(delete_list, self.object_json, self.id_info)
        for item in del_id_list:
            if not self.check_obj_is_not_deleted(item):
                continue
            del_cmd = f"Set-ADObject -Identity \"{item}\" -Remove @{{{param_info}}}"
            code, out, err = execute_cmd(cmd=[self.ps_loc, del_cmd], timeout=120, cmd_array_flag=True)
            if code != "0":
                log.error(f"err_code:{code}; error_out:{out}; err:{err}")
                return False
        return True

    def change_member(self, obj_id, new_info):
        old_info, _ = get_ad_object_info(self.object_json, value=obj_id)
        res, param_list = check_need_change_member(old_info, new_info, self.object_json, self.id_info)
        add_list, delete_list, change_list = param_list[0], param_list[1], param_list[2]
        if not res:
            return True
        if add_list:
            value = format_value("member", add_list)
            set_cmd = f"Set-ADObject -Identity \"{obj_id}\" -Add @{{member={value}}}"
        elif delete_list:
            value = format_value("member", delete_list)
            set_cmd = f"Set-ADObject -Identity \"{obj_id}\" -Remove @{{member={value}}}"
        elif change_list:
            value = format_value("member", change_list)
            set_cmd = f"Set-ADObject -Identity \"{obj_id}\" -Replace @{{member={value}}}"
        else:
            return True
        code, out, err = execute_cmd(cmd=[self.ps_loc, set_cmd], timeout=120, cmd_array_flag=True)
        if code != "0":
            log.error(f"err_code:{code}; error_out:{out}; err:{err}")
            return False
        return True

    def rename_ad_object(self, obj_id, new_info):
        old_info = get_obj_param_info(self.object_json, obj_id)
        res, obj_name = check_need_to_rename(old_info, new_info)
        if not res:
            return True
        rename_cmd = f"Rename-ADObject -Identity \"{obj_id}\" -NewName {obj_name}"
        log.info(f"rename ad-object:{rename_cmd}")
        code, out, err = execute_cmd(cmd=[self.ps_loc, rename_cmd], timeout=120, cmd_array_flag=True)
        if code != "0":
            log.error(f"err_code:{code}; error_out:{out}; err:{err}")
            return False
        return True

    def report_running(self):
        log.info("report ADObject restore task running")
        sub_job_details = SubJobDetails(taskId=self.job_id, subTaskId=self.sub_id,
                                        taskStatus=SubJobStatusEnum.RUNNING.value, progress=NumberConst.FIFTY)
        report_job_details_by_rpc(self.req_id, self.job_id, sub_job_details.dict(by_alias=True))

    def report_progress(self, progress):
        log.info(f"Report restore process,job_id:{self.job_id}")
        log_detail = LogDetail(logInfo=ADDSLabel.ADDS_RESTORE_RUNNING_LABEL,
                               logInfoParam=[str(progress)],
                               logLevel=LogLevel.INFO)
        sub_job_details = SubJobDetails(taskId=self.job_id, subTaskId=self.sub_id, logDetail=[log_detail],
                                        taskStatus=SubJobStatusEnum.RUNNING.value, progress=progress)
        report_job_details_by_rpc(self.req_id, self.job_id, sub_job_details.dict(by_alias=True))

    def calculate_progress(self, progress=0):
        current_progress = 30 + int(progress * 0.7)
        return current_progress

    def wait_post_complete(self):
        """
        功能描述：通过检查是否存在后置任务完成文件，判断任务是否已完成，已完成后重启主机
        """
        log.info(f'execute wait_post_complete, job_id:{self.job_id},sub_job_id:{self.sub_id}')
        result_path = ParamConstant.RESULT_PATH
        file_name = f"adds_{self.job_id}_post_complete"
        post_complete_file = os.path.join(result_path, file_name)
        while True:
            if not os.path.exists(post_complete_file):
                continue
            log.info("Find post job completed file, ready to remove it and exist.")
            time.sleep(30)
            break
        remove_file(post_complete_file)
        log.info("Remove completed file success.")

    def write_post_complete(self):
        """
        功能描述：后置任务上报完成后写上报完成文件
        """
        log.info(f'execute write_post_complete, job_id:{self.job_id}')
        result_path = ParamConstant.RESULT_PATH
        file_name = f"adds_{self.job_id}_post_complete"
        post_complete_file = os.path.join(result_path, file_name)
        if not os.path.exists(post_complete_file):
            flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
            modes = stat.S_IWUSR | stat.S_IRUSR
            with os.fdopen(os.open(post_complete_file, flags, modes), 'w') as m_file:
                m_file.write("post job complete!")
            log.info("write post complete file success")
        return True

    def get_version(self, target_path):
        """
        功能描述：通过wbadmin获取AD备份文件的version版本
        """
        ps_command = "wbadmin get versions -backupTarget:" + target_path
        log.info(f"ps_command: {ps_command}")
        process = subprocess.Popen([self.ps_loc, ps_command], stdout=subprocess.PIPE)
        version = ""
        pattern = r'\b(\d{2}/\d{2}/\d{4}-\d{2}:\d{2})\b'
        while True:
            output = process.stdout.readline()
            if output == b'' and process.poll() is not None:
                log.info("Run get versions success")
                break
            if not output.strip():
                continue
            output_str = output.strip().decode(encoding=self.encoding_mode)
            log.debug(f"output_str: {output_str}")
            version = re.search(pattern, output_str)
            if version:
                version = version.group(1)
                log.info(f"Find version is: {version}")
                break
        process.communicate(timeout=NumberConst.THIRTY)
        cmd_code = process.returncode
        log.info(f"cmd_code: {cmd_code}")
        return version

    def report_sub_job_success(self):
        log.info(f"{self.job_id} restore success")
        log_detail = LogDetail(logInfo=ADDSLabel.PLUGIN_RESTORE_SUBJOB_SUCCESS_LABEL,
                               logInfoParam=[self.sub_id],
                               logLevel=LogLevel.INFO.value)
        sub_job_details = SubJobDetails(taskId=self.job_id, subTaskId=self.sub_id, progress=100, logDetail=[log_detail],
                                        taskStatus=SubJobStatusEnum.COMPLETED.value).dict(by_alias=True)
        report_job_details_by_rpc(self.req_id, self.job_id, sub_job_details)

    def report_sub_job_failed(self, error_code):
        log.error(
            f"Report restore failed, job_id: {self.job_id}, sub_job_id: {self.sub_id}, error code: {error_code}")
        # 获取错误码对应的label和label参数进行上报
        if not self.log_label:
            if self.task_err_code or self.task_err_description:
                log_detail = LogDetail(logInfo=ADDSLabel.EXECUTE_RESTORE_SYSTEM_FAILED_LABEL,
                                       logInfoParam=[self.task_err_code, self.task_err_description],
                                       logLevel=LogLevel.ERROR, logDetail=0)
            else:
                log_detail = LogDetail(logInfo=ADDSLabel.EXECUTE_RESTORE_FAILED_LABEL,
                                       logInfoParam=[self.sub_id], logLevel=LogLevel.ERROR, logDetail=0)
        else:
            log_detail = LogDetail(logInfo=self.log_label, logInfoParam=self.log_label_param,
                                   logLevel=LogLevel.ERROR, logDetail=0)
        sub_job_details = SubJobDetails(taskId=self.job_id, subTaskId=self.sub_id, progress=100,
                                        logDetail=[log_detail],
                                        taskStatus=SubJobStatusEnum.FAILED.value).dict(by_alias=True)
        report_job_details_by_rpc(self.req_id, self.job_id, sub_job_details)

    def report_restore_start(self):
        """
        功能描述：上报开始执行子任务
        """
        log_detail = LogDetail(logInfo=ADDSLabel.START_RESTORE_LABEL, logInfoParam=[self.sub_id],
                               logLevel=DBLogLevel.INFO.value)
        sub_job_details = SubJobDetails(taskId=self.job_id, subTaskId=self.sub_id, progress=0, logDetail=[log_detail],
                                        taskStatus=SubJobStatusEnum.RUNNING.value).dict(by_alias=True)
        report_job_details_by_rpc(self.req_id, self.sub_id, sub_job_details)

    def report_scan_progress(self, number):
        log.info(f"report_restore_scan_process,job_id:{self.job_id}")
        log_detail = LogDetail(logInfo=ADDSLabel.ADDS_SYSTEM_TASK_SCAN, logInfoParam=[str(number)],
                               logLevel=LogLevel.INFO)
        sub_job_details = SubJobDetails(taskId=self.job_id, subTaskId=self.sub_id, logDetail=[log_detail],
                                        taskStatus=SubJobStatusEnum.RUNNING.value, progress=NumberConst.FIFTY)
        report_job_details_by_rpc(self.req_id, self.job_id, sub_job_details.dict(by_alias=True))

    def report_scan_completed(self):
        log.info(f"report_restore_scan_completed,job_id:{self.job_id}")
        log_detail = LogDetail(logInfo=ADDSLabel.ADDS_SYSTEM_TASK_SCAN_COMPLETED, logLevel=LogLevel.INFO)
        sub_job_details = SubJobDetails(taskId=self.job_id, subTaskId=self.sub_id, logDetail=[log_detail],
                                        taskStatus=SubJobStatusEnum.RUNNING.value, progress=NumberConst.FIFTY)
        report_job_details_by_rpc(self.req_id, self.job_id, sub_job_details.dict(by_alias=True))

    def restore_post_job(self):
        """
        功能描述：后置任务
        """
        log.info(f"backup_post_job start, job id:{self.job_id}.")
        response = ResultInfo()
        self.report_result(response.dict(by_alias=True))
        file_path = self.cache_path + f"/restore_flag_{self.job_id}"
        if os.path.exists(file_path):
            os.remove(file_path)
        return True

    def restore_post_job_progress(self):
        """
         功能描述：备份后置任务进度上报
        """
        log.info(f"restore_post_job_progress success, job id:{self.job_id}.")
        progress = SubJobDetails(taskId=self.job_id, subTaskId=self.sub_id, progress=100,
                                 taskStatus=SubJobStatusEnum.COMPLETED.value)
        self.report_result(progress.dict(by_alias=True))
        if self.restore_type != RestoreType.FINE_GRAINED_RESTORE.value and not is_normal_mode():
            # 整体恢复且在目录恢复模式下，写后置完成文件用于标志任务完成，重启主机
            self.write_post_complete()
        return True

    def check_restore_task_running(self):
        cmd = "Get-WBJob"
        code, out, err = execute_cmd([self.ps_loc, cmd], timeout=30, cmd_array_flag=True)
        if code == "0" and "Running" in out.strip():
            log.info("other backup or recovery tasks are running on the computer.")
            return True
        return False

    def check_restore_task_status(self):
        cmd = "Get-WBJob | ConvertTo-Json"
        report_time = int(time.time())
        progress = 0
        while True:
            code, out, err = execute_cmd([self.ps_loc, cmd], timeout=30, cmd_array_flag=True)
            if code != "0":
                log.error(f"err_code:{code}; error_out:{out}; err:{err}")
                return False
            task_info = json.loads(str(out))
            if "StartTime" not in task_info.keys() or not task_info["StartTime"]:
                break
            job_state = task_info["JobState"]
            if job_state == 3:
                self.report_sub_job_success()
                break
            current_operation = task_info["CurrentOperation"]
            match_info = re.match(r"(\d+)%", current_operation)
            if match_info:
                ori_progress = int(match_info.group(1))
                progress = self.calculate_progress(ori_progress)
            else:
                match_info = re.findall(r"\d+", current_operation)
                if match_info:
                    progress = self.calculate_progress()
            current_time = int(time.time())
            if current_time - report_time >= 60:
                self.report_progress(progress)
                report_time = current_time
        return True

    def check_restore_task_status_fine_grained(self):
        file_path = self.cache_path + f"/restore_flag_{self.job_id}"
        while True:
            if not os.path.exists(file_path):
                return False
            with open(file_path, 'r') as f_read:
                info = f_read.read().strip()
            if info == TaskStatus.RUNNING.value:
                continue
            elif info == TaskStatus.COMPLETED.value:
                return True
            else:
                return False

    def update_ad_obj_restored(self, state):
        mode = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
        flag = os.O_WRONLY | os.O_CREAT | os.O_APPEND
        file_path = self.cache_path + f"/restore_flag_{self.job_id}"
        try:
            with os.fdopen(os.open(file_path, flag, mode), 'w', encoding='utf-8') as f:
                f.write(state)
        except Exception as err:
            log.error(err, exc_info=True)
            return False
        log.info(f"update task status:{state}")
        return True

    def check_param_change(self, param_list):
        if not param_list:
            return False
        for param in param_list:
            if len(param) > 0:
                return True
        return False

    def set_object_member_and_memberof(self, obj_list):
        start_time = int(time.time())
        for obj in obj_list:
            current_time = int(time.time())
            if current_time - start_time > 60:
                self.report_running()
                start_time = current_time
            log.info(f"restore obj: {obj}")
            obj_id, distinguishedname = parse_object_id(self.object_json, obj, self.object_from)
            current_info = self.check_obj_is_not_deleted(distinguishedname)
            if not current_info:
                continue
            memberof_result = self.change_memberof(obj_id, distinguishedname, current_info)
            member_result = self.change_member(obj_id, current_info)
            if not member_result or not memberof_result:
                log.error(f"object:{distinguishedname} set member or memberof failed.")

    def validate_backup_path(self):
        """验证备份路径是否存在"""
        if not os.path.exists(self.data_path):
            log.error(f"Path {self.data_path} is not exist")
            return False
        return True

    def handle_completion(self, output_str, process):
        self.auth_restore()
        self.report_sub_job_success()
        self.wait_post_complete()
        log.info("Job is complete, restart computer now")
        if "redémarrage de l'ordinateur" in output_str:
            process.stdin.write(b'O\n')
        elif "reinicialização do computador" in output_str:
            process.stdin.write(b'S\n')
        else:
            process.stdin.write(b'Y\n')
        process.stdin.flush()

    def check_win_langage(self, output_str):
        if ("computer restart" in output_str or "redémarrage de l'ordinateur" in output_str):
            return True
        elif ("reinicialização do computador" in output_str or "重新启动计算机" in output_str):
            return True
        else:
            return False
        
    def get_auth_times(self):
        """获取权威恢复的次数，恢复时必须必上次恢复多执行一次权威恢复"""
        file_name = f"restore_times_file"
        target_dir = os.path.dirname(self.cache_path.rstrip("\\/"))
        file_path = os.path.join(target_dir, file_name)
        auth_times = 1
        try:
            # 查找标志文件是否存在，不存在则是第一次恢复，创建文件
            if not os.path.exists(file_path):
                flags = os.O_WRONLY | os.O_CREAT
                modes = stat.S_IWUSR | stat.S_IRUSR
                with os.fdopen(os.open(file_path, flags, modes), 'w') as f:
                    f.write("1")
                log.info(f"file {file_path} does not exist, create it")
                return auth_times
            # 存在，则在之前的基础上在多做一次权威恢复，将次数写回文件
            with open(file_path, "r+") as f:
                auth_times = int(f.read().strip()) + 1
                f.seek(0)
                f.write(str(auth_times))
                #清除多余内容
                f.truncate()
                log.info(f"file {file_path} exists, times got {auth_times}")
        except Exception as e:
            log.error(f"get times failed, error occurs: {e}")
            self.report_sub_job_failed(self.error_code)
        return auth_times
